notes

#image optimization

`blurDataUrl` 만들기2023. 6. 2.

갯츠비는 알아서 해주지만 넥스트는 해줘야 되기 때문에...

import { decode, encode } from "blurhash"
import sharp from "sharp"

const loadImageData = async (src: string) => {
  const response = await fetch(src)
  if (!response.ok)
    throw new Error(
      `Failed to load image: ${response.status} ${response.statusText}`
    )

  const imageBuffer = await response.arrayBuffer()

  const { data, info } = await sharp(imageBuffer)
    .ensureAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true })

  return {
    data: new Uint8ClampedArray(data),
    width: info.width,
    height: info.height,
  }
}

export const encodeImageToBlurhash = async (imageUrl: string) => {
  const { data, width, height } = await loadImageData(imageUrl)
  return encode(data, width, height, 4, 4)
}

export const blurhashToBase64 = async (
  blurhash: string,
  width: number,
  height: number
) => {
  const pixels = decode(blurhash, width, height)
  const webp = sharp(Buffer.from(pixels), {
    raw: { width, height, channels: 4 },
  }).webp()
  const dataString = (await webp.toBuffer()).toString("base64")

  return `data:image/png;base64,${dataString}`
}

export const generateBlurDataUrl = async (
  imageUrl: string
): Promise<string | undefined> => {
  try {
    const blurhash = await encodeImageToBlurhash(imageUrl)
    return await blurhashToBase64(blurhash, 4, 4)
  } catch (error) {
    console.error(error)
    return undefined
  }
}
import { decode, encode } from "blurhash"
import sharp from "sharp"

const loadImageData = async (src: string) => {
  const response = await fetch(src)
  if (!response.ok)
    throw new Error(
      `Failed to load image: ${response.status} ${response.statusText}`
    )

  const imageBuffer = await response.arrayBuffer()

  const { data, info } = await sharp(imageBuffer)
    .ensureAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true })

  return {
    data: new Uint8ClampedArray(data),
    width: info.width,
    height: info.height,
  }
}

export const encodeImageToBlurhash = async (imageUrl: string) => {
  const { data, width, height } = await loadImageData(imageUrl)
  return encode(data, width, height, 4, 4)
}

export const blurhashToBase64 = async (
  blurhash: string,
  width: number,
  height: number
) => {
  const pixels = decode(blurhash, width, height)
  const webp = sharp(Buffer.from(pixels), {
    raw: { width, height, channels: 4 },
  }).webp()
  const dataString = (await webp.toBuffer()).toString("base64")

  return `data:image/png;base64,${dataString}`
}

export const generateBlurDataUrl = async (
  imageUrl: string
): Promise<string | undefined> => {
  try {
    const blurhash = await encodeImageToBlurhash(imageUrl)
    return await blurhashToBase64(blurhash, 4, 4)
  } catch (error) {
    console.error(error)
    return undefined
  }
}

여러가지 더 있다.

Extracting Image Dimensions from Remote Sources2023. 5. 25.

Before

  • til을 위해 repo issues를 cms로 쓰고 있었고,
  • issue body render를 위해 github api reponse 중 bodyHTMLdangerously... 에 넣어주고 있었음.
  • 근데 이러면 next가 해주는 이것저것이 아까우므로,
  • api response 중 body(markdown string)을 mdx renderer에 넘겨서 써야겠다고 생각.

Issues

How?

  • probe-image-size라는 라이브러리가 있고,
  • mdx renderer option의 component 설정시 <img /> -> <Image />로 replace 하면서 요걸 사용해서 width/height 정보를 넘겨주면 됨.
  • 코드는 대충 이런식:
    import probe from "probe-image-size"
    // ...
    const components: MDXRemoteProps["components"] = {
      // ...
      // @ts-expect-error <- ts는 아직 async 컴포넌트를 모르지만 우리는 rsc 세계로 넘어왔으므로 ok.
      img: async ({ src, alt }) => {
        if (!src) return null
    
        const { width, height } = await probe(src ?? "")
        //                        ^^^^^ rsc ftw...
    
        if (!width || !height) return null
    
        return <Image src={src} alt={alt ?? ""} width={width} height={height} />
      },
      // ...
    }
    // ...
    import probe from "probe-image-size"
    // ...
    const components: MDXRemoteProps["components"] = {
      // ...
      // @ts-expect-error <- ts는 아직 async 컴포넌트를 모르지만 우리는 rsc 세계로 넘어왔으므로 ok.
      img: async ({ src, alt }) => {
        if (!src) return null
    
        const { width, height } = await probe(src ?? "")
        //                        ^^^^^ rsc ftw...
    
        if (!width || !height) return null
    
        return <Image src={src} alt={alt ?? ""} width={width} height={height} />
      },
      // ...
    }
    // ...

Result

최종 렌더된 이미지의 url을 보면

Screenshot 2023-05-25 at 17 18 40

next가 잘 처리하고 있음을 알 수 있음.

Conclusion

RSC는 대박이다...

Tags